package MD2Model;

import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import java.nio.ShortBuffer;
import java.util.Vector;

import javax.microedition.khronos.opengles.GL10;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.opengl.GLUtils;

public class MD2Model {
	private GL10 gl;
	private Context _context;
	private int _ModelID; // ID from resources
	private int _TexID; // ID from resources
	private int pos = 0;

	private MD2Header header;
	private Skin skins[];
	private TexCoord texCoords[];
	private Triagle triangles[];
	private Frame frames[];
	private int glcmds[];

	private Vector<Animation> animations;
	private FloatBuffer drawVertices;
	private float[] vertexBuffersInterp[];
	private FloatBuffer vertexBuffersNonInterp[];
	private FloatBuffer textureBuffer;
	private ShortBuffer indexBuffer;
	private int tex; // ID of texture after loaded
	private int num_Vertices;

	private int cAnimation = -1; // the index of current animation
	private int FrameStart = 0;
	private int FrameEnd = 0;
	private int FrameIndex = 0;

	private short FPS = 30;
	private long lastTime = 0;
	private float fraction = 0f;
	private final float deltaFraction = 1.0f/2;	//interpolate to double frame
	
	private boolean isInterpolate = false;
	private boolean isActive = false;
	
	
	/*
	 * Constructor
	 * @param modelID is the ID of model file in resource(Ex: R.drawable.modelID)
	 * @param textureID is the ID of texture file in the resources
	 */
	public MD2Model (GL10 gl, Context context, int modelID, int textureID, boolean isInterpolate){
		this.gl = gl;
		_context = context;
		_ModelID = modelID;
		_TexID = textureID;
		this.isInterpolate = isInterpolate;
		header = new MD2Header();
		animations = new Vector<Animation>();
		// load model data
		LoadModel();
		LoadGLTexture(gl, context);

		// initialize textures and indices for the frame
		InitTexturesAndIndices();

		// initialize vertex buffer for frame
		InitVertexBuffer(isInterpolate);
		// set model with the first animation
		
		SetAnimation(0);
		isActive = true;
	}
	
	/*
	 * Set the frame rate of the model
	 */
	public void SetFPS (short FPS){
		this.FPS = FPS;
	}
	
	/*
	 * Load model from the data file
	 */
	private int LoadModel () {
		try {
			InputStream is = _context.getResources().openRawResource(_ModelID);
			int length = is.available();
			byte data[] = new byte[length];
			is.read(data);
			is.close();
			pos = 0;
			header.SetValue(GetInt(data), GetInt(data), GetInt(data), GetInt(data),
							GetInt(data), GetInt(data), GetInt(data), GetInt(data),
							GetInt(data), GetInt(data), GetInt(data), GetInt(data),
							GetInt(data), GetInt(data), GetInt(data), GetInt(data),
							GetInt(data));
			if ((header.ident != 844121161) || (header.version != 8)) {
				// error
				is.close();
			}

			// memory allocation
			skins = new Skin[header.num_skins];
			texCoords = new TexCoord[header.num_st];
			triangles = new Triagle[header.num_tris];
			frames = new Frame[header.num_frames];
			glcmds = new int[header.num_glcmds];

			// read model data
			int i = 0;
			SetOffset(header.offset_skins);
			for (i = 0; i < header.num_skins; i++) {
				skins[i] = new Skin();
				skins[i].SetName(GetUChar(64, data));
			}

			SetOffset(header.offset_st);
			for (i = 0; i < header.num_st; i++) {
				texCoords[i] = new TexCoord();
				texCoords[i].SetValue(GetUShort(data), GetUShort(data));
			}

			SetOffset(header.offset_tris);
			for (i = 0; i < header.num_tris; i++) {
				triangles[i] = new Triagle();
				triangles[i].SetValue(GetUShort(data), GetUShort(data),
						GetUShort(data), GetUShort(data), GetUShort(data),
						GetUShort(data));
			}

			SetOffset(header.offset_glcmds);
			for (i = 0; i < header.num_glcmds; i++) {
				glcmds[i] = GetInt(data);
			}

			// read frames
			SetOffset(header.offset_frames);
			for (i = 0; i < header.num_frames; i++) {
				frames[i] = new Frame();
				// memory allocation for vertices of this frame
				frames[i].SetVerticesSize(header.num_vertices);

				// read frame data
				frames[i].GetScale().SetValue(GetFloat(data), GetFloat(data),
						GetFloat(data));
				frames[i].GetTranslate().SetValue(GetFloat(data),
						GetFloat(data), GetFloat(data));

				frames[i].SetName(GetUChar(16, data));

				for (int j = 0; j < header.num_vertices; j++) {
					frames[i].GetVertex(j).SetValue(GetUChar(data),
							GetUChar(data), GetUChar(data), GetUChar(data));
				}
				this.AddAnimation(i, frames[i].GetName());
				
			} // end of for
		} catch (IOException ioe) {
		}
		
		return 0;
	}
	
	/*
	 * Initialize vertex buffer from model data
	 * @param value is the flag to define the interpolate 
	 */
	private void InitVertexBuffer (boolean value) {
		Frame frame;
		Vertex vertex;
		// *************Init Vertices Buffer********************************
		
	
		if (value) {
			ByteBuffer vbb = ByteBuffer.allocateDirect(num_Vertices * 3 * 4); // vertices
																				// x
																				// 2coords(S,T)
																				// x
																				// 4bytes
			vbb.order(ByteOrder.nativeOrder());
			drawVertices = vbb.asFloatBuffer();
			drawVertices.position(0);
			
			vertexBuffersInterp = new float[header.num_frames][num_Vertices * 3];
			for (int m = 0; m < header.num_frames; m++) { // surf on the
															// VertexBuffers[],
															// begin at index 0
				
				frame = frames[m];
				short idx = 0;
				for (int i = 0; i < header.num_tris; i++) {
					// map each vertex to buffer
					for (int j = 0; j < 3; j++) {
						vertex = frame.GetVertex(triangles[i].GetVertex(j));
						vertexBuffersInterp[m][idx++] = (frame.GetScale().GetX()
								* vertex.GetX() + frame.GetTranslate().GetX());
						vertexBuffersInterp[m][idx++] = (frame.GetScale().GetY()
								* vertex.GetY() + frame.GetTranslate().GetY());
						vertexBuffersInterp[m][idx++] = (frame.GetScale().GetZ()
								* vertex.GetZ() + frame.GetTranslate().GetZ());
					}
				}
			}
		} else {
			vertexBuffersNonInterp = new FloatBuffer[header.num_frames];
			for (int m = 0; m < header.num_frames; m++) { // surf on the
															// VertexBuffers[],
															// begin at index 0
				ByteBuffer vbb = ByteBuffer
						.allocateDirect(num_Vertices * 3 * 4); // vertices x
																// 2coords(S,T)
																// x 4bytes
				vbb.order(ByteOrder.nativeOrder());
				vertexBuffersNonInterp[m] = vbb.asFloatBuffer();
				vertexBuffersNonInterp[m].position(0);

				frame = frames[m];
				for (int i = 0; i < header.num_tris; i++) {
					// map each vertex to buffer
					for (int j = 0; j < 3; j++) {
						vertex = frame.GetVertex(triangles[i].GetVertex(j));
						vertexBuffersNonInterp[m].put(frame.GetScale().GetX()
								* vertex.GetX() + frame.GetTranslate().GetX());
						vertexBuffersNonInterp[m].put(frame.GetScale().GetY()
								* vertex.GetY() + frame.GetTranslate().GetY());
						vertexBuffersNonInterp[m].put(frame.GetScale().GetZ()
								* vertex.GetZ() + frame.GetTranslate().GetZ());
					}
				}
				vertexBuffersNonInterp[m].position(0);
			}
		}
	}

	/*
	 * Initialize the texture and the indices from model data
	 */
	private void InitTexturesAndIndices () {
		int i;
		int j;
		short c = 0;

		// *******************Init Texture and
		// Indices*************************************
		num_Vertices = header.num_tris * 3; // triangle have 3 vertices
		ByteBuffer tbb = ByteBuffer.allocateDirect(num_Vertices * 2 * 4); // vertices
																			// x
																			// 2coords(S,T)
																			// x
																			// 4bytes
		tbb.order(ByteOrder.nativeOrder());
		textureBuffer = tbb.asFloatBuffer();
		textureBuffer.position(0);

		ByteBuffer ibb = ByteBuffer.allocateDirect(num_Vertices * 3 * 2); // vertices
																			// x
																			// 3coords(X,Y,Z)
																			// x
																			// 2bytes
		ibb.order(ByteOrder.nativeOrder());
		indexBuffer = ibb.asShortBuffer();
		indexBuffer.position(0);

		for (i = 0; i < header.num_tris; i++) {
			// map each vertex to buffer
			for (j = 0; j < 3; j++) {
				textureBuffer.put((float) texCoords[triangles[i]
						.GetTexCoordIndex(j)].GetS()
						/ header.skinwidth);
				textureBuffer.put((float) texCoords[triangles[i]
						.GetTexCoordIndex(j)].GetT()
						/ header.skinheight);
				indexBuffer.put(c++);
			}
		}
		textureBuffer.position(0);
		indexBuffer.position(0);
	}

	/*
	 * Get the next frame for render model
	 */
	private int GetNextFrameIndex () {
		if(FrameIndex>=header.num_frames-1)
			return FrameStart;
		if (FrameIndex == FrameEnd)
			return FrameStart;
		else
			return FrameIndex + 1;
	}

	public int NextFrame () {
		FrameIndex = GetNextFrameIndex();
		return FrameIndex;
	}
	
	public int NextFrame (int currentFrame) {
		if(currentFrame>=header.num_frames-1)
			return FrameStart;
		if (currentFrame == FrameEnd)
			return FrameStart;
		else
			return currentFrame + 1;
	}
	

	/*
	 * Set the animation for model
	 * @param index is the index of animation in the model
	 * If index is the same with current index, nothing would happened
	 * After change animation, model is always change to mode action
	 */
	public void SetAnimation (int index) {
		if (index > animations.size() - 1 || index < 0 
				|| (index==cAnimation))
			return;

		cAnimation = index;
		Animation a = animations.elementAt(index);
		FrameStart = a.GetFrameStart();
		FrameEnd = a.GetFrameEnd();
		FrameIndex = FrameStart;
		isActive = true;
	}
	
	public void SetAnimation (int index, int currentFrame) {
		if (index > animations.size() - 1 || index < 0 
				|| (index==cAnimation))
			return;

		cAnimation = index;
		Animation a = animations.elementAt(index);
		FrameStart = a.GetFrameStart();
		FrameEnd = a.GetFrameEnd();
		if (currentFrame >= FrameStart && currentFrame <= FrameEnd)
			FrameIndex = currentFrame;
		else
			FrameIndex = FrameStart;
		isActive = true;
	}

	/*
	 * Get the current animation index
	 * @return the current animation index 
	 */
	public int GetAnimation() {
		return cAnimation;
	}

	public void SetAnimation(String name) {
		int n = animations.size();
		for (int i = 0; i < n; i++) {
			if (animations.elementAt(i).GetName().equals(name)) {
				SetAnimation(i);
				return;
			}
		}
	}
	
	public void SetFrameIndex(int index){
		FrameIndex = index;
	}

	public String GetAnimationName () {
		return animations.elementAt(cAnimation).GetName();
	}

	/*
	 * Set status for model
	 * @param active
	 * set false to stop model action
	 */
	public void SetActive (boolean active){
		isActive = active;
	}
	
	/*
	 * Get the active status of model
	 */
	public boolean IsActive (){
		return isActive;
	}
	
	/*
	 * Calculate for interpolate
	 */
	private void interpolate (float fraction, float[] a, float[] b) {
		drawVertices.position(0);
		for (int i = 0; i < a.length; ++i) {
			drawVertices.put((a[i] * (1 - fraction)) + b[i] * fraction);
		}
		drawVertices.position(0);
	}

	/*
	 * Render the model in mode TRIANGLES
	 */
	public void Render (GL10 gl) {

//		gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
				gl.glVertexPointer(3, GL10.GL_FLOAT, 0, vertexBuffersNonInterp[FrameIndex]);
				NextFrame();


		// enable model's texture
		gl.glEnable(GL10.GL_TEXTURE_2D);
		gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY);
		
		gl.glBindTexture(GL10.GL_TEXTURE_2D, tex);
		gl.glTexCoordPointer(2, GL10.GL_FLOAT, 0, textureBuffer);
		gl.glDrawElements(GL10.GL_TRIANGLES, num_Vertices, GL10.GL_UNSIGNED_SHORT, indexBuffer);
		
		gl.glDisableClientState(GL10.GL_TEXTURE_COORD_ARRAY);
		gl.glDisable(GL10.GL_TEXTURE_2D);
//		gl.glDisableClientState(GL10.GL_VERTEX_ARRAY);
	}
	
	public void Render (GL10 gl, int currentArnim, int currentFrame) {

		SetAnimation(currentArnim);
		FrameIndex = currentFrame;
//		gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
		gl.glVertexPointer(3, GL10.GL_FLOAT, 0, vertexBuffersNonInterp[FrameIndex]);

		// enable model's texture
		gl.glEnable(GL10.GL_TEXTURE_2D);
		gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY);

		gl.glBindTexture(GL10.GL_TEXTURE_2D, tex);
		gl.glTexCoordPointer(2, GL10.GL_FLOAT, 0, textureBuffer);
		gl.glDrawElements(GL10.GL_TRIANGLES, num_Vertices,
				GL10.GL_UNSIGNED_SHORT, indexBuffer);

		gl.glDisableClientState(GL10.GL_TEXTURE_COORD_ARRAY);
		gl.glDisable(GL10.GL_TEXTURE_2D);
//		gl.glDisableClientState(GL10.GL_VERTEX_ARRAY);
}

	/*
	 * Load texture from file for the model
	 */
	private void LoadGLTexture (GL10 gl, Context context) {
		int[] textures = new int[1];
		// Get the texture from the Android resource directory
		InputStream is = context.getResources().openRawResource(_TexID);
		Bitmap bitmap = null;
		try {
			// BitmapFactory is an Android graphics utility for images
			bitmap = BitmapFactory.decodeStream(is);

		} finally {
			// Always clear and close
			try {
				is.close();
				is = null;
			} catch (IOException e) {
			}
		}

		// Generate one texture pointer...
		gl.glGenTextures(1, textures, 0);
		tex = textures[0];
		// ...and bind it to our array
		gl.glBindTexture(GL10.GL_TEXTURE_2D, tex);

		// Create Nearest Filtered Texture
		gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MIN_FILTER,
				GL10.GL_NEAREST);
		gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MAG_FILTER,
				GL10.GL_LINEAR);

		// Different possible texture parameters, e.g. GL10.GL_CLAMP_TO_EDGE
		gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_S,
				GL10.GL_REPEAT);
		gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_T,
				GL10.GL_REPEAT);

		// Use the Android GLUtils to specify a two-dimensional texture image
		// from our bitmap
		GLUtils.texImage2D(GL10.GL_TEXTURE_2D, 0, bitmap, 0);

		// Clean up
		bitmap.recycle();
	}

	/*
	 * Filter the frame and add it to the right animation group
	 */
	private void AddAnimation (int frame, String name) {
		if (animations.size() != 0) { // have any animation in Vector
			Animation a = animations.elementAt(animations.size() - 1);
			if (a.GetName().equals(name)) { // this frame belong the old
											// animation (at last index)
				a.SetFrameEnd(frame);
			} else { // this frame belong new animation
				animations.add(new Animation(frame, name));
			}
		} else { // animation vector is empty
			animations.add(new Animation(frame, name));
		}
	}
	
	public int GetNumAnim (){
		return animations.size();
	}

	public boolean IsInterpolate () {
		return isInterpolate;
	}

	/*
	 * Set the offset for reading data
	 */
	private void SetOffset (int offset) {
		pos = offset;
	}

	/*
	 * Get a char (1 bytes) from the data
	 * increase position by 1 
	 */
	private char GetUChar (byte data[]) { // 1byte
		int b = data[pos++];
		if (b < 0)
			b += 256;
		return (char) b;
	}

	/*
	 * Get the array of char from the data
	 * increase position by a mount of char which be read
	 */
	private char[] GetUChar (int n, byte data[]) { // read n bytes -> n chars
		char r[] = new char[n];
		for (int i = 0; i < n; i++)
			r[i] = GetUChar(data);
		return r;
	}

	/*
	 * Get an unsigned short (2 bytes) from the data
	 * increase position by 2 
	 */
	private short GetUShort (byte data[]) { // 2bytes
		int b1 = data[pos++];
		int b2 = data[pos++];
		if (b1 < 0)
			b1 += 256;
		if (b2 < 0)
			b2 += 256;
		return (short) (b2 * 256 + b1);
	}

	/*
	 * Get an short (2 bytes) from the data
	 * increase position by 2 
	 */
	public short GetShort (byte data[]) { // 2bytes
		int b1 = data[pos++];
		int b2 = data[pos++];
		if (b1 < 0)
			b1 += 256;
		return (short) (b2 * 256 + b1);
	}

	/*
	 * Get an integer (4 bytes) from the data
	 * increase position by 4 
	 */
	private int GetInt (byte data[]) { // 4bytes
		int b1 = data[pos++];
		int b2 = data[pos++];
		int b3 = data[pos++];
		int b4 = data[pos++];
		return (b4 << 24) + ((b3 << 24) >>> 8) + ((b2 << 24) >>> 16)
				+ ((b1 << 24) >>> 24);
	}

	/*
	 * Get a float (4 bytes) from the data
	 * increase position by 4 
	 */
	private float GetFloat (byte data[]) { // 4bytes
		return Float.intBitsToFloat(GetInt(data));
	}
}
